package io.ebeaninternal.dbmigration.ddlgeneration.platform;

import io.ebean.config.DatabaseConfig;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebeaninternal.dbmigration.ddlgeneration.DdlBuffer;
import io.ebeaninternal.dbmigration.ddlgeneration.DdlHandler;
import io.ebeaninternal.dbmigration.migration.AlterColumn;

import java.io.IOException;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class AbstractHanaDdl extends PlatformDdl {

  private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\w+)\\s*\\[\\s*\\]\\s*(\\(\\d+\\))?", Pattern.CASE_INSENSITIVE);

  public AbstractHanaDdl(DatabasePlatform platform) {
    super(platform);
    this.addColumn = "add (";
    this.addColumnSuffix = ")";
    this.alterColumn = "alter (";
    this.alterColumnSuffix = ")";
    this.columnDropDefault = " default null";
    this.columnSetDefault = " default";
    this.columnSetNotnull = " not null";
    this.columnSetNull = " null";
    this.dropColumn = "drop (";
    this.dropColumnSuffix = ")";
    this.dropConstraintIfExists = "drop constraint ";
    this.dropIndexIfExists = "drop index ";
    this.dropSequenceIfExists = "drop sequence ";
    this.dropTableCascade = " cascade";
    this.dropTableIfExists = "drop table ";
    this.fallbackArrayType = "nvarchar(1000)";
    this.historyDdl = new HanaHistoryDdl();
    this.identitySuffix = " generated by default as identity";
  }

  @Override
  public String alterColumnBaseAttributes(AlterColumn alter) {
    String tableName = alter.getTableName();
    String columnName = alter.getColumnName();
    String currentType = alter.getCurrentType();
    String type = alter.getType() != null ? alter.getType() : currentType;
    type = convert(type);
    currentType = convert(currentType);
    boolean notnull = (alter.isNotnull() != null) ? alter.isNotnull() : Boolean.TRUE.equals(alter.isCurrentNotnull());
    String notnullClause = notnull ? " not null" : "";
    String defaultValue = DdlHelp.isDropDefault(alter.getDefaultValue()) ? "null"
      : (alter.getDefaultValue() != null ? alter.getDefaultValue() : alter.getCurrentDefaultValue());
    String defaultValueClause = (defaultValue == null || defaultValue.isEmpty()) ? "" : " default " + defaultValue;

    try {
      DdlBuffer buffer = new BaseDdlBuffer(null);
      if (!isConvertible(currentType, type)) {
        // add an intermediate conversion if possible
        if (isNumberType(currentType)) {
          // numbers can always be converted to decimal
          buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
            .append(" decimal ").append(defaultValueClause).append(notnullClause).append(alterColumnSuffix)
            .endOfStatement();

        } else if (isStringType(currentType)) {
          // strings can always be converted to nclob
          buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
            .append(" nclob ").append(defaultValueClause).append(notnullClause).append(alterColumnSuffix)
            .endOfStatement();
        }
      }

      buffer.append("alter table ").append(tableName).append(" ").append(alterColumn).append(" ").append(columnName)
        .append(" ").append(type).append(defaultValueClause).append(notnullClause).append(alterColumnSuffix);

      return buffer.getBuffer();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public String alterColumnDefaultValue(String tableName, String columnName, String defaultValue) {
    throw new UnsupportedOperationException();
  }

  @Override
  public String alterColumnNotnull(String tableName, String columnName, boolean notnull) {
    return null;
  }

  @Override
  public DdlHandler createDdlHandler(DatabaseConfig config) {
    return new HanaDdlHandler(config, this);
  }

  @Override
  public String alterColumnType(String tableName, String columnName, String type) {
    return null;
  }

  @Override
  protected String convertArrayType(String logicalArrayType) {
    Matcher matcher = ARRAY_PATTERN.matcher(logicalArrayType);
    if (matcher.matches()) {
      return convert(matcher.group(1)) + " array" + (matcher.group(2) == null ? "" : matcher.group(2));
    } else {
      return fallbackArrayType;
    }
  }

  @Override
  public String alterTableAddUniqueConstraint(String tableName, String uqName, String[] columns, String[] nullableColumns) {
    if (nullableColumns == null || nullableColumns.length == 0) {
      return super.alterTableAddUniqueConstraint(tableName, uqName, columns, nullableColumns);
    } else {
      return "-- cannot create unique index \"" + uqName + "\" on table \"" + tableName + "\" with nullable columns";
    }
  }

  @Override
  public String alterTableDropUniqueConstraint(String tableName, String uniqueConstraintName) {
    DdlBuffer buffer = new BaseDdlBuffer(null);
    try {
      buffer.append("delimiter $$").newLine();
      buffer.append("do").newLine();
      buffer.append("begin").newLine();
      buffer.append("declare exit handler for sql_error_code 397 begin end").endOfStatement();
      buffer.append("exec 'alter table ").append(tableName).append(" ").append(dropUniqueConstraint).append(" ")
        .append(maxConstraintName(uniqueConstraintName)).append("'").endOfStatement();
      buffer.append("end").endOfStatement();
      buffer.append("$$");
      return buffer.getBuffer();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public String alterTableDropConstraint(String tableName, String constraintName) {
    return alterTableDropUniqueConstraint(tableName, constraintName);
  }

  /**
   * It is rather complex to delete a column on HANA as there must not exist any
   * foreign keys. That's why we call a user stored procedure here
   */
  @Override
  public void alterTableDropColumn(DdlBuffer buffer, String tableName, String columnName) throws IOException {
    buffer.append("CALL usp_ebean_drop_column('").append(tableName).append("', '").append(columnName).append("')")
      .endOfStatement();
  }

  /**
   * Check if a data type can be converted to another data type. Data types can't
   * be converted if the target type has a lower precision than the source type.
   *
   * @param sourceType The source data type
   * @param targetType the target data type
   * @return {@code true} if the type can be converted, {@code false} otherwise
   */
  private boolean isConvertible(String sourceType, String targetType) {
    if (Objects.equals(sourceType, targetType)) {
      return true;
    }

    if (sourceType == null || targetType == null) {
      return true;
    }

    if ("bigint".equals(sourceType)) {
      if ("integer".equals(targetType) || "smallint".equals(targetType) || "tinyint".equals(targetType)) {
        return false;
      }
    } else if ("integer".equals(sourceType)) {
      if ("smallint".equals(targetType) || "tinyint".equals(targetType)) {
        return false;
      }
    } else if ("smallint".equals(sourceType)) {
      if ("tinyint".equals(targetType)) {
        return false;
      }
    } else if ("double".equals(sourceType)) {
      if ("real".equals(targetType)) {
        return false;
      }
    }

    DbPlatformType dbPlatformSourceType = DbPlatformType.parse(sourceType);

    if ("float".equals(dbPlatformSourceType.getName())) {
      if ("real".equals(targetType)) {
        return false;
      }
    } else if ("varchar".equals(dbPlatformSourceType.getName()) || "nvarchar".equals(dbPlatformSourceType.getName())) {
      DbPlatformType dbPlatformTargetType = DbPlatformType.parse(targetType);
      if ("varchar".equals(dbPlatformTargetType.getName()) || "nvarchar".equals(dbPlatformTargetType.getName())) {
        if (dbPlatformSourceType.getDefaultLength() > dbPlatformTargetType.getDefaultLength()) {
          return false;
        }
      }
    } else if ("decimal".equals(dbPlatformSourceType.getName())) {
      DbPlatformType dbPlatformTargetType = DbPlatformType.parse(targetType);
      if ("decimal".equals(dbPlatformTargetType.getName())) {
        if (dbPlatformSourceType.getDefaultLength() > dbPlatformTargetType.getDefaultLength()
          || dbPlatformSourceType.getDefaultScale() > dbPlatformTargetType.getDefaultScale()) {
          return false;
        }
      }
    }

    return true;
  }

  private boolean isNumberType(String type) {
    return type != null
      && ("bigint".equals(type) || "integer".equals(type) || "smallint".equals(type) || "tinyint".equals(type)
      || type.startsWith("float") || "real".equals(type) || "double".equals(type) || type.startsWith("decimal"));
  }

  private boolean isStringType(String type) {
    return type != null
      && (type.startsWith("varchar") || type.startsWith("nvarchar") || "clob".equals(type) || "nclob".equals(type));
  }
}
